package patch

import (
	"bufio"
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"strings"

	"github.com/devstream-io/devstream/internal/log"
)

const (
	processOptionTabToSpace ProcessOption = "tabToSpace"
	processOptionSpaceToTab ProcessOption = "spaceToTab"
)

type ProcessOption string

// Patch calls the patch command to apply a diff file to an original
func Patch(patchFile string) error {
	log.Infof("Patching file: %s", patchFile)

	// Fix patch file if it mixed tab and space indentation
	err := fixPatchFile(patchFile)
	if err != nil {
		return fmt.Errorf("patch file fix failed: %w", err)
	}

	// Check if the patch command exists
	patchPath, err := exec.LookPath("patch")
	if err != nil {
		return fmt.Errorf("patch command not found: %w", err)
	}

	// Use the patch tool to apply the patch
	cmd := exec.Command(patchPath, "-i", patchFile, "-t", "-p0")
	output, err := cmd.CombinedOutput()
	if err != nil {
		return fmt.Errorf("patch command failed: %w\nOutput: %s", err, string(output))
	}

	log.Infof("Successfully patched the file")
	return nil
}

// checkPatchCommand checks if the patch command exists and is executable
func checkPatchCommand() error {
	// Check if the patch command exists
	path, err := exec.LookPath("patch")
	if err != nil {
		return fmt.Errorf("patch command not found: %w", err)
	}

	// Check if the patch command is executable
	fileInfo, err := os.Stat(path)
	if err != nil {
		return fmt.Errorf("failed to stat patch command: %w", err)
	}

	if fileInfo.Mode()&0111 == 0 {
		return fmt.Errorf("patch command is not executable")
	}

	return nil
}

// fixPatchFile fixes the patch file if it mixed tab and space indentation.
// The patch file is generated by GPT4, and it may have different indentation with the original file.
// The original file path is contained in the patch file, so we can use the fix the patch file by using the original file.
// If the original file uses tab indentation, we replace all spaces with tabs in the patch file.
// If the original file uses space indentation, we replace all tabs with spaces in the patch file.
func fixPatchFile(patchFile string) error {
	// Read the original file path from the patch file
	originalFilePath, err := extractOriginalFilePathFromPatchFile(patchFile)

	if err != nil {
		return fmt.Errorf("failed to extract original file path from patch string: %w", err)
	}

	// Check if the original file contain tabs in the indentation
	original, err := os.Open(originalFilePath)
	if err != nil {
		return fmt.Errorf("failed to open original file: %w", err)
	}
	defer original.Close()

	hasTab := false
	scanner := bufio.NewScanner(original)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.HasPrefix(line, "\t") {
			hasTab = true
			break
		}
	}

	if err = scanner.Err(); err != nil {
		return fmt.Errorf("failed to read original file: %w", err)
	}

	// The original file uses tab indentation
	if hasTab {
		// Replace all space indentation with tabs in the patch file
		if err = processTabSpaceSwitch(patchFile, processOptionSpaceToTab); err != nil {
			return fmt.Errorf("failed to process tab to space: %w", err)
		}
		// The original file uses space indentation
	} else {
		// Replace all tab indentation with spaces in the patch file
		if err = processTabSpaceSwitch(patchFile, processOptionTabToSpace); err != nil {
			return fmt.Errorf("failed to process space to tab: %w", err)
		}
	}

	return nil
}

// ExtractOriginalFilePathFromPatchString extracts the original file path from a patch string
// e.g. --- pkg/patch/patch.go	2021-08-15 16:00:00.000000000 +0900 -> pkg/patch/patch.go
func extractOriginalFilePathFromPatchFile(patchFile string) (string, error) {
	// Read content from the patch file
	fileContent, err := os.ReadFile(patchFile)
	if err != nil {
		return "", fmt.Errorf("failed to read patch file: %w", err)
	}

	lines := strings.Split(string(fileContent), "\n")

	for _, line := range lines {
		if strings.HasPrefix(line, "--- ") {
			fields := strings.Fields(line)
			if len(fields) > 1 {
				return fields[1], nil
			}
		}
	}

	return "", fmt.Errorf("original file path not found in patch string")
}

// processTabSpaceSwitch processes the tab/space indentation switch in a file
// If the option is processOptionTabToSpace, it replaces all tabs with spaces
// If the option is processOptionSpaceToTab, it replaces all spaces with tabs
func processTabSpaceSwitch(filePath string, option ProcessOption) error {
	file, err := os.Open(filePath)
	if err != nil {
		return fmt.Errorf("failed to open file: %w", err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	var processedLines []string

	// Matches the start of the string (^) followed by an optional + or - sign, followed by one or more groups of 4 spaces ( {4})+
	spaceRegex := regexp.MustCompile(`^(\+|\-)?( {4})+`)
	// Matches the start of the string (^) followed by an optional + or - sign, followed by one or more tabs (\t)+
	tabRegex := regexp.MustCompile(`^(\+|\-)?\t+`)

	for scanner.Scan() {
		line := scanner.Text()
		if option == processOptionTabToSpace {
			line = tabRegex.ReplaceAllStringFunc(line, func(s string) string {
				prefix := ""
				if s[0] == '+' || s[0] == '-' {
					prefix = string(s[0])
					s = s[1:]
				}
				return prefix + strings.Repeat("    ", len(s))
			})
		} else if option == processOptionSpaceToTab {
			line = spaceRegex.ReplaceAllStringFunc(line, func(s string) string {
				prefix := ""
				if s[0] == '+' || s[0] == '-' {
					prefix = string(s[0])
					s = s[1:]
				}
				return prefix + strings.Repeat("\t", len(s)/4)
			})
		} else {
			return fmt.Errorf("invalid process option: %s", option)
		}
		processedLines = append(processedLines, line)
	}

	if err = scanner.Err(); err != nil {
		return fmt.Errorf("failed to read file: %w", err)
	}

	err = os.WriteFile(filePath, []byte(strings.Join(processedLines, "\n")+"\n"), 0644)
	if err != nil {
		return fmt.Errorf("failed to write file: %w", err)
	}

	return nil
}
